package com.lsl.rdf.aop;

import com.lsl.rdf.annotation.LockMethod;
import com.lsl.rdf.distributedLock.DistributedLock;
import com.lsl.rdf.lock.LockWaitOverTimeException;
import com.lsl.rdf.utils.LockAopUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.mybatis.spring.SqlSessionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;

/**
 * Created by lsl on 2021/4/27.
 */
@Aspect
@Slf4j
@Order(999)
public class DistributedLockAop {

    @Autowired
    private DistributedLock distributedLock;
    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Pointcut("@annotation(com.lsl.rdf.annotation.LockMethod)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object doLock(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result;

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

        LockMethod lockMethod = methodSignature.getMethod().getAnnotation(LockMethod.class);

        int waitSeconds = lockMethod.waitSeconds();
        int overtimeSeconds = lockMethod.overtimeSeconds();
        boolean clearCache = lockMethod.clearCache();
        boolean fairLock = lockMethod.fairLock();
        String el = lockMethod.el();

        String lockName = LockAopUtil.getLockName(joinPoint, el);

        if (this.getLock(lockName, waitSeconds, overtimeSeconds, fairLock)) {
            this.clearCache(clearCache);
            try {
                result = joinPoint.proceed();
            } finally {
                this.unLock(lockName, fairLock);
            }
        } else {
            log.warn("DistributedLockAop.doLock 等待锁超时. lockName: {}, waitSeconds: {}, overtimeSeconds: {}", lockName, waitSeconds, overtimeSeconds);
            throw new LockWaitOverTimeException(String.format("等待锁超时！ lockName: %s, waitSeconds: %s", lockName, waitSeconds));
        }
        return result;
    }

    /**
     * 获取锁
     *
     * @param lockName        所名称
     * @param waitSeconds     等待锁时间
     * @param overtimeSeconds 锁超时时间
     * @param isFairLock      是否公平锁
     * @return 锁
     */
    private boolean getLock(String lockName, int waitSeconds, int overtimeSeconds, boolean isFairLock) {
        Assert.notNull(lockName, "DistributedLockAop.getLock 获取锁失败 lockName 为空");
        boolean lock;
        if (isFairLock) {
            lock = distributedLock.acquireFairLock(lockName, waitSeconds, overtimeSeconds);
        } else {
            lock = distributedLock.acquireLock(lockName, waitSeconds, overtimeSeconds);
        }
        return lock;
    }

    /**
     * 清除mybatis二级缓存
     *
     * @param clearCache 是否清除
     */
    private void clearCache(boolean clearCache) {
        // 清除一级缓存，double check的时候，一级缓存可能会造成锁不住
        if (clearCache) {
            SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory);
            sqlSession.clearCache();
        }
    }

    /**
     * 释放锁
     *
     * @param lockName 锁名称
     */
    private void unLock(final String lockName, final boolean fairLock) {
        Assert.notNull(lockName, "DistributedLockAop.unLock 解锁失败 lockName 为空");
        // 如果是事务，则在事务提交后再释放锁
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            TransactionSynchronizationManager
                    .registerSynchronization(new TransactionSynchronization() {
                        @Override
                        public void afterCompletion(int status) {
                            if (fairLock) {
                                distributedLock.releaseFairLock(lockName);
                            } else {
                                distributedLock.releaseLock(lockName);
                            }
                        }
                    });
        } else {
            if (fairLock) {
                distributedLock.releaseFairLock(lockName);
            } else {
                distributedLock.releaseLock(lockName);
            }
        }
    }
}
